﻿using FastEmit;
using Flurl;
using Flurl.Http;
using System.Runtime.CompilerServices;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
#if NET461
using System.Net.Http;
#endif

namespace OkFlurl
{
    public class FlurlApi
    {
        #region json

        public JsonSerializerOptions JsonOptionsDefault { get; set; } = new()
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            NumberHandling = JsonNumberHandling.AllowReadingFromString,
            PropertyNameCaseInsensitive = true,
            PropertyNamingPolicy = null,
            DictionaryKeyPolicy = null
        };

        public readonly JsonSerializerOptions JsonOptions = new()
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            NumberHandling = JsonNumberHandling.AllowReadingFromString,
            PropertyNameCaseInsensitive = true,
            PropertyNamingPolicy = null,
            DictionaryKeyPolicy = null
        };

        public readonly JsonSerializerOptions JsonOptionsMs = new()
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            NumberHandling = JsonNumberHandling.AllowReadingFromString,
            PropertyNameCaseInsensitive = true,
            PropertyNamingPolicy = null,
            DictionaryKeyPolicy = null
        };

        public readonly JsonSerializerOptions JsonOptionsNs = new()
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            NumberHandling = JsonNumberHandling.AllowReadingFromString,
            PropertyNameCaseInsensitive = true,
            PropertyNamingPolicy = null,
            DictionaryKeyPolicy = null
        };

        public readonly JsonSerializerOptions JsonOptionsWeb = new()
        {
            //Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), //不编码中文
            //PropertyNamingPolicy = null, //null序列化属性名称不变,小写JsonNamingPolicy.CamelCase
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, //不编码中文和html标签
            NumberHandling = JsonNumberHandling.AllowReadingFromString, //反序列化时允许string转数字
            PropertyNameCaseInsensitive = true, //true反序列化忽略大小写(开销大)
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
        };

        public readonly JsonSerializerOptions JsonOptionsComment = new()
        {
            //Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), //不编码中文
            //PropertyNamingPolicy = null, //序列化属性不转小写
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,//不编码中文和html
            NumberHandling = JsonNumberHandling.AllowReadingFromString, //反序列化时允许string转数字
            PropertyNameCaseInsensitive = true, //反序列化忽略大小写
            PropertyNamingPolicy = null,
            DictionaryKeyPolicy = null,
            AllowTrailingCommas = true, //允许尾部逗号
            ReadCommentHandling = JsonCommentHandling.Skip //跳过注释
        };

        public readonly JsonSerializerOptions JsonOptionsIndented = new()
        {
            //Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), //不编码中文
            //PropertyNamingPolicy = null, //序列化属性不转小写
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,//不编码中文和html
            NumberHandling = JsonNumberHandling.AllowReadingFromString, //反序列化时允许string转数字
            PropertyNameCaseInsensitive = true, //反序列化忽略大小写
            PropertyNamingPolicy = null,
            DictionaryKeyPolicy = null,
            AllowTrailingCommas = true, //允许尾部逗号
            ReadCommentHandling = JsonCommentHandling.Skip, //跳过注释
            WriteIndented = true
        };

        #endregion

        #region prototype

        public string BaseUrl { get; }

        public int? Timeout { get; set; }

        public string HttpVersion { get; set; }

        public string UserName { get; set; }

        public string Password { get; set; }

        public string OAuthBearerToken { get; set; }

        public readonly Dictionary<string, string> Headers = [];

        public readonly Dictionary<string, string> Cookies = [];

        public string TimeFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";

        public Action<FlurlRequest> OnCreateRequest;

        #endregion

        public FlurlApi(string baseUrl)
        {
            BaseUrl = baseUrl;

            JsonOptionsDefault.Converters.Add(new DateTimeConverter());
            JsonOptionsDefault.Converters.Add(new DateTimeOffsetConverter());

            JsonOptions.Converters.Add(new DateTimeConverter());
            JsonOptions.Converters.Add(new DateTimeOffsetConverter());

            JsonOptionsMs.Converters.Add(new DateTimeMsConverter());
            JsonOptionsMs.Converters.Add(new DateTimeOffsetMsConverter());

            JsonOptionsNs.Converters.Add(new DateTimeNsConverter());
            JsonOptionsNs.Converters.Add(new DateTimeOffsetNsConverter());

            JsonOptionsWeb.Converters.Add(new DateTimeConverter());
            JsonOptionsWeb.Converters.Add(new DateTimeOffsetConverter());

            JsonOptionsComment.Converters.Add(new DateTimeConverter());
            JsonOptionsComment.Converters.Add(new DateTimeOffsetConverter());

            JsonOptionsIndented.Converters.Add(new DateTimeConverter());
            JsonOptionsIndented.Converters.Add(new DateTimeOffsetConverter());
        }

        #region method

        public FlurlRequest CreateRequest(string path)
        {
            var req = new FlurlRequest(BaseUrl);
            req.AppendPathSegment(path);
            if (Timeout.HasValue)
            {
                req.WithTimeout(Timeout.Value);
            }

            if (!string.IsNullOrEmpty(HttpVersion))
            {
                req.Settings.HttpVersion = HttpVersion;
            }

            if (!string.IsNullOrEmpty(UserName))
            {
                req.WithBasicAuth(UserName, Password);
            }

            if (!string.IsNullOrEmpty(OAuthBearerToken))
            {
                req.WithOAuthBearerToken(OAuthBearerToken);
            }

            if (Headers.Count > 0)
            {
                foreach (var item in Headers)
                {
                    req.WithHeader(item.Key, item.Value);
                }
            }

            if (Cookies.Count > 0)
            {
                foreach (var item in Cookies)
                {
                    req.WithCookie(item.Key, item.Value);
                }
            }

            OnCreateRequest?.Invoke(req);
            return req;
        }

        public void InitGetPar(FlurlRequest req, object par)
        {
            if (par == null)
            {
                return;
            }

            if (par is IDictionary<string, string> dict)
            {
                foreach (var item in dict)
                {
                    if (item.Value == null)
                    {
                        continue;
                    }

                    req.SetQueryParam(item.Key, item.Value);
                }
                return;
            }

            if (par is Dictionary<string, object> dict2)
            {
                foreach (var item in dict2)
                {
                    var val = item.Value;

                    if (val == null)
                    {
                        continue;
                    }

                    var t = val.GetType();
                    if (t == typeof(string))
                    {
                        req.SetQueryParam(item.Key, (string)val);
                    }
                    else if (t == typeof(DateTime))
                    {
                        var dt = (DateTime)val;
                        req.SetQueryParam(item.Key, dt.ToString(TimeFormat));
                    }
                    else if (t == typeof(DateTimeOffset))
                    {
                        var dt = (DateTimeOffset)val;
                        req.SetQueryParam(item.Key, dt.ToString(TimeFormat));
                    }
                    else if (t == typeof(DateTime?))
                    {
                        var dt = (DateTime?)val;
                        req.SetQueryParam(item.Key, dt.Value.ToString(TimeFormat));
                    }
                    else if (t == typeof(DateTimeOffset?))
                    {
                        var dt = (DateTimeOffset?)val;
                        req.SetQueryParam(item.Key, dt.Value.ToString(TimeFormat));
                    }
                    else
                    {
                        req.SetQueryParam(item.Key, val.ToString());
                    }
                }
                return;
            }

            if (par is Dictionary<string, JsonElement> dict3)
            {
                foreach (var item in dict3)
                {
                    var txt = item.Value.GetRawText();
                    if (txt == null)
                    {
                        continue;
                    }
                    req.SetQueryParam(item.Key, txt);
                }
                return;
            }

            var accessor = FastEmitFactory.GetTypeAccessor(par.GetType());
            foreach (var item in accessor.MemberDict)
            {
                var val = accessor.GetValue(par, item.Key);
                if (val == null)
                {
                    continue;
                }

                var t = item.Value;
                if (t == typeof(string))
                {
                    req.SetQueryParam(item.Key, (string)val);
                }
                else if (t == typeof(DateTime))
                {
                    var dt = (DateTime)val;
                    req.SetQueryParam(item.Key, dt.ToString(TimeFormat));
                }
                else if (t == typeof(DateTimeOffset))
                {
                    var dt = (DateTimeOffset)val;
                    req.SetQueryParam(item.Key, dt.ToString(TimeFormat));
                }
                else if (t == typeof(DateTime?))
                {
                    var dt = (DateTime?)val;
                    req.SetQueryParam(item.Key, dt.Value.ToString(TimeFormat));
                }
                else if (t == typeof(DateTimeOffset?))
                {
                    var dt = (DateTimeOffset?)val;
                    req.SetQueryParam(item.Key, dt.Value.ToString(TimeFormat));
                }
                else
                {
                    req.SetQueryParam(item.Key, val.ToString());
                }
            }
        }

        public void InitFormPar(FlurlForm form, object par)
        {
            if (par == null)
            {
                return;
            }

            if (par is IDictionary<string, string> dict)
            {
                foreach (var item in dict)
                {
                    if (item.Value == null)
                    {
                        continue;
                    }

                    form.AddString(item.Key, item.Value);
                }
                return;
            }

            if (par is Dictionary<string, object> dict2)
            {
                foreach (var item in dict2)
                {
                    var val = item.Value;

                    if (val == null)
                    {
                        continue;
                    }

                    var t = val.GetType();
                    if (t == typeof(string))
                    {
                        form.AddString(item.Key, (string)val);
                    }
                    else if (t == typeof(DateTime))
                    {
                        var dt = (DateTime)val;
                        form.AddString(item.Key, dt.ToString(TimeFormat));
                    }
                    else if (t == typeof(DateTimeOffset))
                    {
                        var dt = (DateTimeOffset)val;
                        form.AddString(item.Key, dt.ToString(TimeFormat));
                    }
                    else if (t == typeof(DateTime?))
                    {
                        var dt = (DateTime?)val;
                        form.AddString(item.Key, dt.Value.ToString(TimeFormat));
                    }
                    else if (t == typeof(DateTimeOffset?))
                    {
                        var dt = (DateTimeOffset?)val;
                        form.AddString(item.Key, dt.Value.ToString(TimeFormat));
                    }
                    else
                    {
                        form.AddString(item.Key, val.ToString());
                    }
                }
                return;
            }

            if (par is Dictionary<string, JsonElement> dict3)
            {
                foreach (var item in dict3)
                {
                    var txt = item.Value.GetRawText();
                    if (txt == null)
                    {
                        continue;
                    }
                    form.AddString(item.Key, txt);
                }
                return;
            }


            var accessor = FastEmitFactory.GetTypeAccessor(par.GetType());
            foreach (var item in accessor.MemberDict)
            {
                var val = accessor.GetValue(par, item.Key);
                if (val == null)
                {
                    continue;
                }

                var t = item.Value;
                if (t == typeof(string))
                {
                    form.AddString(item.Key, (string)val);
                }
                else if (t == typeof(DateTime))
                {
                    var dt = (DateTime)val;
                    form.AddString(item.Key, dt.ToString(TimeFormat));
                }
                else if (t == typeof(DateTimeOffset))
                {
                    var dt = (DateTimeOffset)val;
                    form.AddString(item.Key, dt.ToString(TimeFormat));
                }
                else if (t == typeof(DateTime?))
                {
                    var dt = (DateTime?)val;
                    form.AddString(item.Key, dt.Value.ToString(TimeFormat));
                }
                else if (t == typeof(DateTimeOffset?))
                {
                    var dt = (DateTimeOffset?)val;
                    form.AddString(item.Key, dt.Value.ToString(TimeFormat));
                }
                else
                {
                    form.AddString(item.Key, val.ToString());
                }
            }

        }

        #endregion

        #region GET

        public async Task<IFlurlResponse> GetAsync(FlurlRequest req, object par, HttpCompletionOption hco = HttpCompletionOption.ResponseContentRead, CancellationToken token = default)
        {
            InitGetPar(req, par);
            return await req.GetAsync(hco, token);
        }

        public async Task<string> GetStringAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var res = await GetAsync(req, par, token: token);
            return await res.GetStringAsync();
        }

        public async Task<Stream> GetStreamAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            var res = await GetAsync(req, par, HttpCompletionOption.ResponseHeadersRead, token);
            try
            {
                return await res.GetStreamAsync();
            }
            catch
            {
                res?.Dispose();
                throw;
            }
        }

        public async Task<T> GetJsonAsync<T>(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var stream = await GetStreamAsync(req, par, token);
            return await JsonSerializer.DeserializeAsync<T>(stream, JsonOptionsDefault, token);
        }

        public async IAsyncEnumerable<T> GetJsonStreamAsync<T>(FlurlRequest req, object par, [EnumeratorCancellation] CancellationToken token = default)
        {
            using var stream = await GetStreamAsync(req, par, token);
            var unbuffer = JsonSerializer.DeserializeAsyncEnumerable<T>(stream, JsonOptionsDefault, token);
            await foreach (var item in unbuffer)
            {
                yield return item;
            }
        }

        public async Task<byte[]> GetBytesAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var res = await GetAsync(req, par, token: token);
            return await res.GetBytesAsync();
        }

        public async Task<(Stream stream, string name, long? length)> GetDownloadStreamAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            var res = await GetAsync(req, par, HttpCompletionOption.ResponseHeadersRead, token);
            try
            {
                var stream = await res.GetStreamAsync();
                var name = FlurlExt.GetFileName(req, res);
                var length = res.ResponseMessage.Content.Headers.ContentLength;
                return (stream, name, length);
            }
            catch
            {
                res?.Dispose();
                throw;
            }
        }

        public async Task<(byte[] bytes, string name)> GetDownloadBytesAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var res = await GetAsync(req, par, token: token);
            var bytes = await res.GetBytesAsync();
            var name = FlurlExt.GetFileName(req, res);
            return (bytes, name);
        }

        public async Task<string> GetDownloadDiskAsync(FlurlRequest req, object par, string folderPath, string fileName = null, FlurlProgress progress = null, int bufferSize = 4096, CancellationToken token = default)
        {
            var (stream, name, length) = await GetDownloadStreamAsync(req, par, token);
            if (string.IsNullOrEmpty(fileName))
            {
                fileName = name;
            }

            using (stream)
            {
                if (progress != null)
                {
                    return await FlurlExt.SaveToDiskAsync(stream, folderPath, fileName, length, progress, bufferSize, token);
                }
                else
                {
                    return await FlurlExt.SaveToDiskAsync(stream, folderPath, fileName, bufferSize, token);
                }
            }
        }

        #endregion

        #region POST

        public async Task<IFlurlResponse> PostAsync(FlurlRequest req, object par, HttpCompletionOption hco = HttpCompletionOption.ResponseContentRead, CancellationToken token = default)
        {
            if (par == null)
            {
                return await req.PostAsync(null, hco, token);
            }

            if (par is StringContent scc)
            {
                return await req.PostAsync(scc, hco, token);
            }

            if (par is byte[] bytes)
            {
                using var brc = new ByteArrayContent(bytes);
                return await req.PostAsync(brc, hco, token);
            }

            if (par is FlurlStreamContent fs)
            {
                return await req.PostAsync(fs, hco, token);
            }

            if (par is StreamContent sc)
            {
                return await req.PostAsync(sc, hco, token);
            }

            if (par is Stream stream)
            {
                return await req.PostAsync(new StreamContent(stream), hco, token);
            }

            if (par is string str)
            {
                return await req.PostStringAsync(str, hco, token);
            }

            //"text/plain; charset=UTF-8"
            //req.WithHeader("Content-Type", "application/json; charset=UTF-8");
            req.WithHeader("Content-Type", "application/json; charset=UTF-8");
            using var fsc = new FlurlStreamContent(async sendStream =>
            {
                await JsonSerializer.SerializeAsync(sendStream, par, JsonOptionsDefault, token);
            });
            return await req.PostAsync(fsc, hco, token);
        }

        public async Task<string> PostGetStringAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var res = await PostAsync(req, par, token: token);
            return await res.GetStringAsync();
        }

        public async Task<Stream> PostGetStreamAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            var res = await PostAsync(req, par, HttpCompletionOption.ResponseHeadersRead, token);
            try
            {
                return await res.GetStreamAsync();
            }
            catch
            {
                res?.Dispose();
                throw;
            }
        }

        public async Task<T> PostGetJsonAsync<T>(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var stream = await PostGetStreamAsync(req, par, token);
            return await JsonSerializer.DeserializeAsync<T>(stream, JsonOptionsDefault, token);
        }

        public async IAsyncEnumerable<T> PostGetJsonStreamAsync<T>(FlurlRequest req, object par, [EnumeratorCancellation] CancellationToken token = default)
        {
            using var stream = await PostGetStreamAsync(req, par, token);
            var unbuffer = JsonSerializer.DeserializeAsyncEnumerable<T>(stream, JsonOptionsDefault, token);
            await foreach (var item in unbuffer)
            {
                yield return item;
            }
        }

        public async Task<byte[]> PostGetBytesAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var res = await PostAsync(req, par, token: token);
            return await res.GetBytesAsync();
        }

        public async Task<(Stream stream, string name, long? length)> PostDownloadStreamAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            var res = await PostAsync(req, par, HttpCompletionOption.ResponseHeadersRead, token);
            try
            {
                var stream = await res.GetStreamAsync();
                var name = FlurlExt.GetFileName(req, res);
                var length = res.ResponseMessage.Content.Headers.ContentLength;
                return (stream, name, length);
            }
            catch
            {
                res?.Dispose();
                throw;
            }
        }

        public async Task<(byte[] bytes, string name)> PostDownloadBytesAsync(FlurlRequest req, object par, CancellationToken token = default)
        {
            using var res = await PostAsync(req, par, token: token);
            var bytes = await res.GetBytesAsync();
            var name = FlurlExt.GetFileName(req, res);
            return (bytes, name);
        }

        public async Task<string> PostDownloadDiskAsync(FlurlRequest req, object par, string folderPath, string fileName = null, FlurlProgress progress = null, int bufferSize = 4096, CancellationToken token = default)
        {
            var (stream, name, length) = await PostDownloadStreamAsync(req, par, token);
            if (string.IsNullOrEmpty(fileName))
            {
                fileName = name;
            }

            using (stream)
            {
                if (progress != null)
                {
                    return await FlurlExt.SaveToDiskAsync(stream, folderPath, fileName, length, progress, bufferSize, token);
                }
                else
                {
                    return await FlurlExt.SaveToDiskAsync(stream, folderPath, fileName, bufferSize, token);
                }
            }
        }

        #endregion

        #region Form

        public Task<IFlurlResponse> FormAsync(FlurlRequest req, object par, FlurlForm form = null, HttpCompletionOption hco = HttpCompletionOption.ResponseContentRead, CancellationToken token = default)
        {
            form ??= new();
            form.Token = token;
            InitFormPar(form, par);
            return req.SendAsync(HttpMethod.Post, form.Multipart, hco, token);
        }

        public async Task<string> FormGetStringAsync(FlurlRequest req, object par, FlurlForm form = null, CancellationToken token = default)
        {
            using var res = await FormAsync(req, par, form, token: token);
            return await res.GetStringAsync();
        }

        public async Task<Stream> FormGetStreamAsync(FlurlRequest req, object par, FlurlForm form = null, CancellationToken token = default)
        {
            var res = await FormAsync(req, par, form, HttpCompletionOption.ResponseHeadersRead, token);
            try
            {
                return await res.GetStreamAsync();
            }
            catch
            {
                res?.Dispose();
                throw;
            }
        }

        public async Task<T> FormGetJsonAsync<T>(FlurlRequest req, object par, FlurlForm form = null, CancellationToken token = default)
        {
            using var stream = await FormGetStreamAsync(req, par, form, token);
            return await JsonSerializer.DeserializeAsync<T>(stream, JsonOptionsDefault, token);
        }

        public async IAsyncEnumerable<T> FormGetJsonStreamAsync<T>(FlurlRequest req, object par, FlurlForm form = null, [EnumeratorCancellation] CancellationToken token = default)
        {
            using var stream = await FormGetStreamAsync(req, par, form, token);
            var unbuffer = JsonSerializer.DeserializeAsyncEnumerable<T>(stream, JsonOptionsDefault, token);
            await foreach (var item in unbuffer)
            {
                yield return item;
            }
        }

        public async Task<byte[]> FormGetBytesAsync(FlurlRequest req, object par, FlurlForm form = null, CancellationToken token = default)
        {
            using var res = await FormAsync(req, par, form, token: token);
            return await res.GetBytesAsync();
        }

        public async Task<(Stream stream, string name, long? length)> FormDownloadStreamAsync(FlurlRequest req, object par, FlurlForm form = null, CancellationToken token = default)
        {
            var res = await FormAsync(req, par, form, HttpCompletionOption.ResponseHeadersRead, token);
            try
            {
                var stream = await res.GetStreamAsync();
                var name = FlurlExt.GetFileName(req, res);
                var length = res.ResponseMessage.Content.Headers.ContentLength;
                return (stream, name, length);
            }
            catch
            {
                res?.Dispose();
                throw;
            }
        }

        public async Task<(byte[] bytes, string name)> FormDownloadBytesAsync(FlurlRequest req, object par, FlurlForm form = null, CancellationToken token = default)
        {
            using var res = await FormAsync(req, par, form, token: token);
            var bytes = await res.GetBytesAsync();
            var name = FlurlExt.GetFileName(req, res);
            return (bytes, name);
        }

        public async Task<string> FormDownloadDiskAsync(FlurlRequest req, object par, string folderPath, string fileName = null, FlurlForm form = null, int bufferSize = 4096, CancellationToken token = default)
        {
            var (stream, name, length) = await FormDownloadStreamAsync(req, par, form, token);
            if (string.IsNullOrEmpty(fileName))
            {
                fileName = name;
            }

            using (stream)
            {
                if (form.DownloadProgress != null)
                {
                    return await FlurlExt.SaveToDiskAsync(stream, folderPath, fileName, length, form.DownloadProgress, form.DownloadBufferSize, token);
                }
                else
                {
                    return await FlurlExt.SaveToDiskAsync(stream, folderPath, fileName, bufferSize, token);
                }
            }
        }

        #endregion
    }
}
